package main

import (
	"bufio"
	"flag"
	"fmt"
	jsoniter "github.com/json-iterator/go"
	"io"
	"io/ioutil"
	"moonton.net/iaptool/common"
	"moonton.net/iaptool/tokens"
	"net/http"
	"os"
	"strings"
	"time"
)

type GoogleReceiptData struct {
	OrderId             string `json:"orderId"`
	PackageName         string `json:"packageName"`
	ProductId           string `json:"productId"`
	PurchaseTime        uint64 `json:"purchaseTime"`
	PurchaseState       int    `json:"purchaseState"`
	PurchaseToken       string `json:"purchaseToken"`
	ObfuscatedAccountId string `json:"obfuscatedAccountId"`
	ObfuscatedProfileId string `json:"obfuscatedProfileId"`
}

type SubscriptionPurchase struct {
	// AcknowledgementState: The acknowledgement state of the subscription
	// product. Possible values are: 0. Yet to be acknowledged 1.
	// Acknowledged
	AcknowledgementState int64 `json:"acknowledgementState,omitempty"`

	// AutoRenewing: Whether the subscription will automatically be renewed
	// when it reaches its current expiry time.
	AutoRenewing bool `json:"autoRenewing,omitempty"`

	// AutoResumeTimeMillis: Time at which the subscription will be
	// automatically resumed, in milliseconds since the Epoch. Only present
	// if the user has requested to pause the subscription.
	AutoResumeTimeMillis int64 `json:"autoResumeTimeMillis,omitempty,string"`

	// CancelReason: The reason why a subscription was canceled or is not
	// auto-renewing. Possible values are: 0. User canceled the subscription
	// 1. Subscription was canceled by the system, for example because of a
	// billing problem 2. Subscription was replaced with a new subscription
	// 3. Subscription was canceled by the developer
	CancelReason int64 `json:"cancelReason,omitempty"`

	// CancelSurveyResult: Information provided by the user when they
	// complete the subscription cancellation flow (cancellation reason
	// survey).
	CancelSurveyResult *SubscriptionCancelSurveyResult `json:"cancelSurveyResult,omitempty"`

	// CountryCode: ISO 3166-1 alpha-2 billing country/region code of the
	// user at the time the subscription was granted.
	CountryCode string `json:"countryCode,omitempty"`

	// DeveloperPayload: A developer-specified string that contains
	// supplemental information about an order.
	DeveloperPayload string `json:"developerPayload,omitempty"`

	// EmailAddress: The email address of the user when the subscription was
	// purchased. Only present for purchases made with 'Subscribe with
	// Google'.
	EmailAddress string `json:"emailAddress,omitempty"`

	// ExpiryTimeMillis: Time at which the subscription will expire, in
	// milliseconds since the Epoch.
	ExpiryTimeMillis int64 `json:"expiryTimeMillis,omitempty,string"`

	// ExternalAccountId: User account identifier in the third-party
	// service. Only present if account linking happened as part of the
	// subscription purchase flow.
	ExternalAccountId string `json:"externalAccountId,omitempty"`

	// FamilyName: The family name of the user when the subscription was
	// purchased. Only present for purchases made with 'Subscribe with
	// Google'.
	FamilyName string `json:"familyName,omitempty"`

	// GivenName: The given name of the user when the subscription was
	// purchased. Only present for purchases made with 'Subscribe with
	// Google'.
	GivenName string `json:"givenName,omitempty"`

	// IntroductoryPriceInfo: Introductory price information of the
	// subscription. This is only present when the subscription was
	// purchased with an introductory price. This field does not indicate
	// the subscription is currently in introductory price period.
	IntroductoryPriceInfo *IntroductoryPriceInfo `json:"introductoryPriceInfo,omitempty"`

	// Kind: This kind represents a subscriptionPurchase object in the
	// androidpublisher service.
	Kind string `json:"kind,omitempty"`

	// LinkedPurchaseToken: The purchase token of the originating purchase
	// if this subscription is one of the following: 0. Re-signup of a
	// canceled but non-lapsed subscription 1. Upgrade/downgrade from a
	// previous subscription For example, suppose a user originally signs up
	// and you receive purchase token X, then the user cancels and goes
	// through the resignup flow (before their subscription lapses) and you
	// receive purchase token Y, and finally the user upgrades their
	// subscription and you receive purchase token Z. If you call this API
	// with purchase token Z, this field will be set to Y. If you call this
	// API with purchase token Y, this field will be set to X. If you call
	// this API with purchase token X, this field will not be set.
	LinkedPurchaseToken string `json:"linkedPurchaseToken,omitempty"`

	// ObfuscatedExternalAccountId: An obfuscated version of the id that is
	// uniquely associated with the user's account in your app. Present for
	// the following purchases: * If account linking happened as part of the
	// subscription purchase flow. * It was specified using
	// https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setobfuscatedaccountid when the purchase was
	// made.
	ObfuscatedExternalAccountId string `json:"obfuscatedExternalAccountId,omitempty"`

	// ObfuscatedExternalProfileId: An obfuscated version of the id that is
	// uniquely associated with the user's profile in your app. Only present
	// if specified using
	// https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setobfuscatedprofileid when the purchase was
	// made.
	ObfuscatedExternalProfileId string `json:"obfuscatedExternalProfileId,omitempty"`

	// OrderId: The order id of the latest recurring order associated with
	// the purchase of the subscription.
	OrderId string `json:"orderId,omitempty"`

	// PaymentState: The payment state of the subscription. Possible values
	// are: 0. Payment pending 1. Payment received 2. Free trial 3. Pending
	// deferred upgrade/downgrade
	PaymentState int64 `json:"paymentState,omitempty"`

	// PriceAmountMicros: Price of the subscription, not including tax.
	// Price is expressed in micro-units, where 1,000,000 micro-units
	// represents one unit of the currency. For example, if the subscription
	// price is €1.99, price_amount_micros is 1990000.
	PriceAmountMicros int64 `json:"priceAmountMicros,omitempty,string"`

	// PriceChange: The latest price change information available. This is
	// present only when there is an upcoming price change for the
	// subscription yet to be applied. Once the subscription renews with the
	// new price or the subscription is canceled, no price change
	// information will be returned.
	PriceChange *SubscriptionPriceChange `json:"priceChange,omitempty"`

	// PriceCurrencyCode: ISO 4217 currency code for the subscription price.
	// For example, if the price is specified in British pounds sterling,
	// price_currency_code is "GBP".
	PriceCurrencyCode string `json:"priceCurrencyCode,omitempty"`

	// ProfileId: The Google profile id of the user when the subscription
	// was purchased. Only present for purchases made with 'Subscribe with
	// Google'.
	ProfileId string `json:"profileId,omitempty"`

	// ProfileName: The profile name of the user when the subscription was
	// purchased. Only present for purchases made with 'Subscribe with
	// Google'.
	ProfileName string `json:"profileName,omitempty"`

	// PromotionCode: The promotion code applied on this purchase. This
	// field is only set if a vanity code promotion is applied when the
	// subscription was purchased.
	PromotionCode string `json:"promotionCode,omitempty"`

	// PromotionType: The type of promotion applied on this purchase. This
	// field is only set if a promotion is applied when the subscription was
	// purchased. Possible values are: 0. One time code 1. Vanity code
	PromotionType int64 `json:"promotionType,omitempty"`

	// PurchaseType: The type of purchase of the subscription. This field is
	// only set if this purchase was not made using the standard in-app
	// billing flow. Possible values are: 0. Test (i.e. purchased from a
	// license testing account) 1. Promo (i.e. purchased using a promo code)
	PurchaseType *int64 `json:"purchaseType,omitempty"`

	// StartTimeMillis: Time at which the subscription was granted, in
	// milliseconds since the Epoch.
	StartTimeMillis int64 `json:"startTimeMillis,omitempty,string"`

	// UserCancellationTimeMillis: The time at which the subscription was
	// canceled by the user, in milliseconds since the epoch. Only present
	// if cancelReason is 0.
	UserCancellationTimeMillis int64 `json:"userCancellationTimeMillis,omitempty,string"`
}
type SubscriptionCancelSurveyResult struct {
	// CancelSurveyReason: The cancellation reason the user chose in the
	// survey. Possible values are: 0. Other 1. I don't use this service
	// enough 2. Technical issues 3. Cost-related reasons 4. I found a
	// better app
	CancelSurveyReason int64 `json:"cancelSurveyReason,omitempty"`

	// UserInputCancelReason: The customized input cancel reason from the
	// user. Only present when cancelReason is 0.
	UserInputCancelReason string `json:"userInputCancelReason,omitempty"`
}
type IntroductoryPriceInfo struct {
	// IntroductoryPriceAmountMicros: Introductory price of the
	// subscription, not including tax. The currency is the same as
	// price_currency_code. Price is expressed in micro-units, where
	// 1,000,000 micro-units represents one unit of the currency. For
	// example, if the subscription price is €1.99, price_amount_micros is
	// 1990000.
	IntroductoryPriceAmountMicros int64 `json:"introductoryPriceAmountMicros,omitempty,string"`

	// IntroductoryPriceCurrencyCode: ISO 4217 currency code for the
	// introductory subscription price. For example, if the price is
	// specified in British pounds sterling, price_currency_code is "GBP".
	IntroductoryPriceCurrencyCode string `json:"introductoryPriceCurrencyCode,omitempty"`

	// IntroductoryPriceCycles: The number of billing period to offer
	// introductory pricing.
	IntroductoryPriceCycles int64 `json:"introductoryPriceCycles,omitempty"`

	// IntroductoryPricePeriod: Introductory price period, specified in ISO
	// 8601 format. Common values are (but not limited to) "P1W" (one week),
	// "P1M" (one month), "P3M" (three months), "P6M" (six months), and
	// "P1Y" (one year).
	IntroductoryPricePeriod string `json:"introductoryPricePeriod,omitempty"`
}
type SubscriptionPriceChange struct {
	// NewPrice: The new price the subscription will renew with if the price
	// change is accepted by the user.
	NewPrice *Price `json:"newPrice,omitempty"`

	// State: The current state of the price change. Possible values are: 0.
	// Outstanding: State for a pending price change waiting for the user to
	// agree. In this state, you can optionally seek confirmation from the
	// user using the In-App API. 1. Accepted: State for an accepted price
	// change that the subscription will renew with unless it's canceled.
	// The price change takes effect on a future date when the subscription
	// renews. Note that the change might not occur when the subscription is
	// renewed next.
	State int64 `json:"state,omitempty"`
}
type Price struct {
	// Currency: 3 letter Currency code, as defined by ISO 4217. See
	// java/com/google/common/money/CurrencyCode.java
	Currency string `json:"currency,omitempty"`

	// PriceMicros: Price in 1/million of the currency base unit,
	// represented as a string.
	PriceMicros string `json:"priceMicros,omitempty"`
}

func getSubscriptionsPurchase(receipt *GoogleReceiptData) (*SubscriptionPurchase, error) {
	token, err := tokens.GetCacheToken("gp")
	if err != nil {
		return nil, err
	}

	resource := fmt.Sprintf("https://androidpublisher.googleapis.com/androidpublisher/v3/applications/%s/purchases/subscriptions/%s/tokens/%s",
		receipt.PackageName,
		receipt.ProductId,
		receipt.PurchaseToken)

	// check
	u := resource + "?access_token=" + token
	//fmt.Printf("get purchase url: %s\n", u)
	resp, err := http.Get(u)
	if err != nil {
		return nil, fmt.Errorf("请求请阅接口失败 %s", err)
	}
	respData, err := ioutil.ReadAll(resp.Body)
	resp.Body.Close()
	if err != nil {
		return nil, fmt.Errorf("读取订阅接口返回失败 %s", err)
	}

	//fmt.Printf("get purchase resp: %s\n", string(respData))
	var purchase SubscriptionPurchase
	err = jsoniter.Unmarshal(respData, &purchase)
	if err != nil {
		return nil, fmt.Errorf("解析订阅接口返回失败 %s", err)
	}

	if purchase.OrderId != "" {
		return &purchase, err
	}

	return nil, fmt.Errorf("接口返回错误, rsp:%s", strings.Replace(string(respData), "\n", "", -1))
}

type ProductsPurchase struct {
	Kind                        string `json:"kind,omitempty"`
	PurchaseTimeMillis          string `json:"purchaseTimeMillis,omitempty"`
	PurchaseState               int    `json:"purchaseState,omitempty"`
	ConsumptionState            int    `json:"consumptionState,omitempty"`
	DeveloperPayload            string `json:"developerPayload,omitempty"`
	OrderId                     string `json:"orderId,omitempty"`
	PurchaseType                int    `json:"purchaseType,omitempty"`         //可能的值包括：0。测试（即从许可测试帐号中购买的服务）1.促销（即使用促销代码购买）2.激励广告
	AcknowledgementState        int    `json:"acknowledgementState,omitempty"` //应用内商品的确认状态。可能的值包括：0。尚未确认 1. 已确认
	PurchaseToken               string `json:"purchaseToken,omitempty"`
	ProductId                   string `json:"productId,omitempty"`
	Quantity                    int    `json:"quantity,omitempty"`
	ObfuscatedExternalAccountId string `json:"obfuscatedExternalAccountId,omitempty"`
	ObfuscatedExternalProfileId string `json:"obfuscatedExternalProfileId,omitempty"`
	RegionCode                  string `json:"regionCode,omitempty"`
}

func getPurchase(receipt *GoogleReceiptData) (*ProductsPurchase, error) {
	token, err := tokens.GetCacheToken("gp")
	if err != nil {
		return nil, err
	}

	resource := fmt.Sprintf("https://androidpublisher.googleapis.com/androidpublisher/v3/applications/%s/purchases/products/%s/tokens/%s",
		receipt.PackageName,
		receipt.ProductId,
		receipt.PurchaseToken)

	// check
	u := resource + "?access_token=" + token
	//fmt.Printf("get purchase url: %s\n", u)
	resp, err := http.Get(u)
	if err != nil {
		return nil, fmt.Errorf("请求请阅接口失败 %s", err)
	}
	respData, err := ioutil.ReadAll(resp.Body)
	resp.Body.Close()
	if err != nil {
		return nil, fmt.Errorf("读取订阅接口返回失败 %s", err)
	}

	//fmt.Printf("get purchase resp: %s\n", string(respData))
	var purchase ProductsPurchase
	err = jsoniter.Unmarshal(respData, &purchase)
	if err != nil {
		return nil, fmt.Errorf("解析订阅接口返回失败 %s, %s", err, string(respData))
	}

	if purchase.OrderId != "" {
		return &purchase, err
	}

	return nil, fmt.Errorf("接口返回错误, rsp:%s", strings.Replace(string(respData), "\n", "", -1))
}
func getUnconsumedPurchase(packageName string) {
	token, err := tokens.GetCacheToken("gp")
	if err != nil {
		fmt.Println("-----------error--------------", err)
		return
	}

	fmt.Println("-----------or--------------")
	//resource := fmt.Sprintf("https://www.googleapis.com/androidpublisher/v3/applications/%s/purchases/products", packageName)
	//resource := fmt.Sprintf("https://www.googleapis.com/androidpublisher/v3/applications/%s/purchases/voidedpurchases", packageName)
	resource := fmt.Sprintf("https://androidpublisher.googleapis.com/androidpublisher/v3/applications/%s/purchases/products", packageName)
	//resource := fmt.Sprintf("https://androidpublisher.googleapis.com/androidpublisher/v3/applications/%s/purchases/products/%s/tokens/%s",
	//	receipt.PackageName,
	//	receipt.ProductId,
	//	receipt.PurchaseToken)

	helper := common.HttpHelper{
		//Header: http.Header{
		//	"Authorization": []string{fmt.Sprintf("Bearer %s", token)},
		//},
		ApiUrl: resource + "?access_token=" + token,
		Method: "GET",
	}
	rspData, err := helper.Do()
	if err != nil {
		fmt.Println("-----------error--------------", err)
		return
	}
	fmt.Println("-------------------------")
	fmt.Println(string(rspData))
	fmt.Println("-------------------------")
	var response map[string]interface{}
	err = jsoniter.Unmarshal(rspData, &response)
	if err != nil {
		fmt.Println("解析JSON响应失败:", err)
		return
	}

	//var rspJson struct {
	//	NotificationID string `json:"testNotificationToken"`
	//}
	//err = jsoniter.Unmarshal(rspData, &rspJson)
	//if err != nil {
	//	return
	//}
	fmt.Println(response)
}

// SUBSCRIPTION_CANCELED               = 3  // - 自愿或非自愿地取消了订阅。如果是自愿取消，在用户取消时发送。
func cancelSubscription(receipt *GoogleReceiptData) error {
	token, err := tokens.GetCacheToken("gp")
	if err != nil {
		return err
	}

	resource := fmt.Sprintf("https://androidpublisher.googleapis.com/androidpublisher/v3/applications/%s/purchases/subscriptions/%s/tokens/%s:cancel",
		receipt.PackageName,
		receipt.ProductId,
		receipt.PurchaseToken)
	u := resource + "?access_token=" + token
	//fmt.Printf("cancel subscription url: %s\n", u)

	helper := common.HttpHelper{
		//Header: http.Header{
		//	"Authorization": []string{fmt.Sprintf("Bearer %s", token)},
		//},
		ApiUrl: u,
		Method: "POST",
	}
	_, err = helper.Do()
	if err != nil {
		return err
	}

	return nil
}

// 收到以下通知
//
//	SUBSCRIPTION_REVOKED                = 12 // - 用户在有效时间结束前已撤消订阅。
//	SUBSCRIPTION_EXPIRED                = 13 // - 订阅已过期。
func revokeSubscription(receipt *GoogleReceiptData) error {
	token, err := tokens.GetCacheToken("gp")
	if err != nil {
		return err
	}

	resource := fmt.Sprintf("https://androidpublisher.googleapis.com/androidpublisher/v3/applications/%s/purchases/subscriptions/%s/tokens/%s:revoke",
		receipt.PackageName,
		receipt.ProductId,
		receipt.PurchaseToken)
	u := resource + "?access_token=" + token
	fmt.Printf("revoke subscription url: %s\n", u)

	helper := common.HttpHelper{
		//Header: http.Header{
		//	"Authorization": []string{fmt.Sprintf("Bearer %s", token)},
		//},
		ApiUrl: u,
		Method: "POST",
	}
	_, err = helper.Do()
	if err != nil {
		return err
	}

	return nil
}

func parserSubscriptionFile(fileName string) (_err error, _gpSubscriptionData []*GoogleReceiptData) {
	file, _err := os.Open(fileName)
	if _err != nil {
		return fmt.Errorf("open file:%s error %v,", fileName, _err), nil
	}
	defer file.Close()
	r := bufio.NewReader(file)
	for {
		line, _err := r.ReadString('\n')
		if _err != nil && _err != io.EOF {
			return fmt.Errorf("read file:%s error %v,", fileName, _err), nil
		}
		if _err == io.EOF {
			break
		}

		line = strings.TrimSpace(line)
		params := common.SplitString(line, " ", true)
		if len(params) != 3 {
			return fmt.Errorf("subscription format not correct,size:%d \n %v", len(params), line), nil
		}

		var googleReceiptData GoogleReceiptData
		_err = jsoniter.UnmarshalFromString(params[2], &googleReceiptData)
		if _err != nil {
			return fmt.Errorf("subscription format not correct, error:%v \n %v", _err, line), nil
		}
		if googleReceiptData.OrderId == "" {
			return fmt.Errorf("receipt data format error, struct:%+v, line:%v", googleReceiptData, line), nil
		}

		_gpSubscriptionData = append(_gpSubscriptionData, &googleReceiptData)
	}

	return
}

func CancelGoogleSubscription(fileName *string, needCancel *bool) {
	if nil == fileName || needCancel == nil {
		fmt.Printf("unknown error\n")
		return
	}
	err, orders := parserSubscriptionFile(*fileName)
	if err != nil {
		fmt.Printf("订阅订单文件解析错误 %s\n", err)
		return
	}

	os.Remove("checkFail")
	os.Remove("checkOk")
	os.Remove("cancelOk")
	os.Remove("cancelFail")

	var checkFail string
	var checkOk string
	var okOrders []*GoogleReceiptData
	// 是否是自动续订
	for _, order := range orders {
		purchase, err := getSubscriptionsPurchase(order)
		if err != nil {
			checkFail += fmt.Sprintf("%+v %s\n", *order, err)
			continue
		}
		//fmt.Printf("获取订单 %+v\n", purchase)
		if purchase.AutoRenewing == false || uint32(purchase.ExpiryTimeMillis/1000) < uint32(time.Now().Unix()) {
			checkFail += fmt.Sprintf("%+v AutoRenewing:%v ExpireTime:%d->%s\n", *order, purchase.AutoRenewing, purchase.ExpiryTimeMillis/1000, time.Unix(purchase.ExpiryTimeMillis/1000, 0).Format("2006-01-02 15:04:05"))
		} else {
			checkOk += fmt.Sprintf("%+v %+v\n", *order, purchase)
			okOrders = append(okOrders, order)
		}
	}
	if len(checkOk) != 0 {
		ioutil.WriteFile("checkOk", []byte(checkOk), 0644)
	}
	if len(checkFail) != 0 {
		ioutil.WriteFile("checkFail", []byte(checkFail), 0644)
	}
	if *needCancel == false {
		return
	}

	var cancelOk string
	var cancelFail string
	for _, order := range okOrders {
		err = cancelSubscription(order)
		if err != nil {
			cancelFail += fmt.Sprintf("%+v %s\n", *order, err)
		} else {
			cancelOk += fmt.Sprintf("%+v\n", *order)
		}
	}

	if len(cancelOk) != 0 {
		ioutil.WriteFile("cancelOk", []byte(cancelOk), 0644)
	}
	if len(cancelFail) != 0 {
		ioutil.WriteFile("cancelFail", []byte(cancelFail), 0644)
	}
}

func IosTransactionLookup(order string) {
	payloads, err := LookupAppleReceiptInfoByUserOrder(order)
	if err != nil {
		fmt.Printf("order:%s, error:%s\n", order, err.Error())
		return
	}
	if payloads == nil {
		fmt.Printf("order:%s not exist\n", order)
		return
	}

	var result string
	for _, transaction := range payloads {
		revocationDate := common.FormatTime(uint32(transaction.RevocationDate / 1000))
		if transaction.RevocationDate == 0 {
			revocationDate = ""
		}
		result += fmt.Sprintf("OriginalTransactionId:%s TransactionId:%s ProductId:%s PurchaseDate:%s revocationDate:%s RevocationReason:%d srcData:%+v\n",
			transaction.OriginalTransactionId, transaction.TransactionId, transaction.ProductId, common.FormatTime(uint32(transaction.PurchaseDate/1000)),
			revocationDate, transaction.RevocationReason, *transaction)
	}
	fmt.Println(result)
}

func main() {
	fileName := flag.String("file", "", "subscription file name")
	needCancel := flag.Bool("cancel", false, "default check subscription order, if true, it will cancel subscription order")
	funcType := flag.String("func", "", "in [CancelGoogleSubscription,IosTransactionLookup]")
	order := flag.String("order", "", "apple app user's order")
	flag.Parse()
	if *funcType == "" {
		fmt.Println("please chosse a func in  [CancelGoogleSubscription,IosTransactionLookup]")
		flag.Usage()
		os.Exit(1)
	}
	switch *funcType {
	case "CancelGoogleSubscription":
		CancelGoogleSubscription(fileName, needCancel)
	case "IosTransactionLookup":
		if *order == "" {
			fmt.Println("please input apple app user's order")
			os.Exit(1)
		}
		IosTransactionLookup(*order)
	default:
		fmt.Println("please chosse a func in  [CancelGoogleSubscription,IosTransactionLookup]")
		flag.Usage()
		os.Exit(1)
	}
}
